﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Macds.Core.ServiceLocator;
using System.Reflection;
using System.Collections;

namespace Macds.Core.IoC
{
    public class DependencyFactory
    {
        private ObjectsConfigurationSection configuration;
        private ObjectsConfigurationGroup configurationGroup;
        private Dictionary<string, ObjectElement> elements;
        private Dictionary<string, object> objects;

        public DependencyFactory(bool useConfigurationGroup)
        {
            LoadInit(useConfigurationGroup);
        }

        public DependencyFactory()
        {
            LoadInit(false);
        }

        public T Create<T>(string name)
        {
            // Load object
            var element = GetObjectElement(name);

            // Check cache object
            if (element.CacheEnabled)
            {
                if (this.objects.ContainsKey(name))
                {
                    return (T)this.objects[name];
                }
            }

            T obj = Create<T>(element);

            // Load properties
            if (element.Properties != null)
            {
                foreach (PropertyElement property in element.Properties)
                {
                    if (property.Single)
                    {
                        if (!String.IsNullOrEmpty(property.Object))
                        {
                            Type genericType = obj.GetType().GetProperty(property.Name).PropertyType;
                            object objInstance = CreateObjectRecursiveForProperty(genericType, property.Object);
                            AssignProperty(obj, property.Name, objInstance, true);
                        }
                        else
                        {
                            if (property.Value != null)
                            {
                                AssignProperty(obj, property.Name, property.Value, false);
                            }
                        }
                    }
                    else
                    {
                        if (property.Items != null)
                        {
                            if (property.Items.Count > 0)
                            {
                                 var listProperty = obj.GetType().GetProperty(property.Name);
                                var listType = typeof(List<>);
                                var genericArgs = listProperty.PropertyType.GetGenericArguments();
                                var concreteType = listType.MakeGenericType(genericArgs);
                                var items = (IList)Activator.CreateInstance(concreteType);

                                foreach (ItemElement item in property.Items)
                                {
                                    var internalElement = GetObjectElement(item.Object);
                                    string className = internalElement.Class + ", " + internalElement.Assembly;

                                    Type interfaceType = Type.GetType(className, true, false);
                                    object itemInstance = CreateObjectRecursiveForProperty(interfaceType.GetInterfaces()[0], item.Object);
                                    items.Add(itemInstance);
                                }

                                AssignProperty(obj, property.Name, items, true);
                            }
                        }

                        if (property.Values != null)
                        {
                            if (property.Values.Count > 0)
                            {
                                string valueType = property.ValueType;
                                if(String.IsNullOrEmpty(valueType))
                                    valueType = property.Values[0].TypeName;

                                Type objectType1 = Type.GetType(valueType, true, false);
                                var values = (IList)typeof(List<>).MakeGenericType(new Type[] { objectType1 })
                                    .GetConstructor(Type.EmptyTypes)
                                    .Invoke(null);

                                foreach (ValueElement value in property.Values)
                                {
                                    values.Add(Convert.ChangeType(value.Data, objectType1));
                                }

                                AssignProperty(obj, property.Name, values, true);
                            }
                        }
                    }
                }
            }

            // Add to cache
            if (element.CacheEnabled)
            {
                if (!this.objects.ContainsKey(name))
                {
                    this.objects.Add(name, obj);
                }
            }

            return obj;
        }

        #region Private Methods

        private void LoadInit(bool useConfigurationGroup)
        {
            this.elements = new Dictionary<string, ObjectElement>();
            this.objects = new Dictionary<string, object>();

            if (useConfigurationGroup)
            {
                this.configurationGroup = ObjectsConfigurationGroup.Load();
            }
            else
            {
                this.configuration = ObjectsConfigurationSection.Load();
            }
        }
        
        private object CreateObjectRecursiveForProperty(Type genericType, string objectName)
        {
            var method = typeof(DependencyFactory).GetMethod("Create");
            var generic = method.MakeGenericMethod(genericType);
            return generic.Invoke(this, new object[] { objectName });
        }

        private T Create<T>(ObjectElement element)
        {
            LocatorTarget target = new LocatorTarget(element.Assembly, element.Class);

            if (element.Parameters != null)
            {
                var objects = new List<object>();
                foreach (ParameterElement parameter in element.Parameters)
                {
                    if (!String.IsNullOrEmpty(parameter.Value))
                    {
                        if (String.IsNullOrEmpty(parameter.ValueType))
                            throw new NullReferenceException("The valueType can't be null when a value is provided");

                        Type objectType = Type.GetType(parameter.ValueType, true, false);
                        object paramObject = Convert.ChangeType(parameter.Value, objectType);
                        objects.Add(paramObject);
                    }
                    else
                    {
                        if (String.IsNullOrEmpty(parameter.Object))
                            throw new NullReferenceException("The object can't be null when a value is not provided");

                        var objElement = GetObjectElement(parameter.Object);
                        var assembly = Assembly.Load(objElement.Assembly);
                        Type objectType = assembly.GetType(objElement.Class);

                        object paramObject = CreateObjectRecursiveForProperty(objectType, parameter.Object);
                        objects.Add(paramObject);
                    }
                }

                target = new LocatorTarget(element.Assembly, element.Class, objects.ToArray());
            }
            
            var locator = new Locator(target);
            return (T)locator.Service;
        }

        private ObjectElement GetObjectElement(string name)
        {
            if(configuration != null)
            {
                var element = this.configuration.Objects[name];
                if (element == null)
                    throw new NullReferenceException(String.Format("The object {0} does not exists in the configuration file", name));

                if (!String.IsNullOrEmpty(element.Type))
                {
                    var classType = this.configuration.Types[element.Type];
                    element.Class = classType.Class;
                    element.Assembly = classType.Assembly;
                }

                return element;
            }
            else
            {
                var element = this.configurationGroup.GetObject(name);
                if (element == null)
                    throw new NullReferenceException(String.Format("The object {0} does not exists in the configuration file", name));

                if (!String.IsNullOrEmpty(element.Type))
                {
                    var classType = this.configurationGroup.GetClassType(element.Type);
                    element.Class = classType.Class;
                    element.Assembly = classType.Assembly;
                }

                return element;
            }
            #region old code
            /*if (!elements.ContainsKey(name))
            {
                if (configuration != null)
                {
                    var element = this.configuration.Objects[name];

                    if (!String.IsNullOrEmpty(element.Type))
                    {
                        var classType = this.configuration.Types[element.Type];
                        element.Class = classType.Class;
                        element.Assembly = classType.Assembly;
                    }

                    elements.Add(name, element);
                }
                else
                {
                    var element = this.configurationGroup.GetObject(name);

                    if (!String.IsNullOrEmpty(element.Type))
                    {
                        var classType = this.configurationGroup.GetClassType(element.Type);
                        element.Class = classType.Class;
                        element.Assembly = classType.Assembly;
                    }

                    elements.Add(name, element);
                }
            }

            return elements[name];*/
            #endregion
        }

        private void AssignProperty(object obj, string name, object property, bool isObject)
        {
            PropertyInfo prop = obj.GetType().GetProperty(name);
            if (prop == null)
                throw new NullReferenceException(String.Format("The property {0} does not exists in the object {1}", name, obj.ToString().Split('.').Last()));

            if (prop.CanWrite)
            {
                if (isObject)
                {
                    prop.SetValue(obj, property, null);
                }
                else
                {
                    // Added enum handling
                    bool isEnum = typeof(Enum).IsAssignableFrom(prop.PropertyType);

                    if (!isEnum)
                    {
                        prop.SetValue(obj, Convert.ChangeType(property, prop.PropertyType), null);
                    }
                    else
                    {
                        prop.SetValue(obj, System.Enum.Parse(prop.PropertyType, property.ToString()), null);
                    }
                }
            }
        }

        #endregion
    }
}
